import { generateSecureRandomString } from "./crypto";
import { templateIdentity } from "./strings";

export function createUrlIfValid(...args: ConstructorParameters<typeof URL>) {
  try {
    return new URL(...args);
  } catch (e) {
    return null;
  }
}
import.meta.vitest?.test("createUrlIfValid", ({ expect }) => {
  // Test with valid URLs
  expect(createUrlIfValid("https://example.com")).toBeInstanceOf(URL);
  expect(createUrlIfValid("https://example.com/path?query=value#hash")).toBeInstanceOf(URL);
  expect(createUrlIfValid("/path", "https://example.com")).toBeInstanceOf(URL);

  // Test with invalid URLs
  expect(createUrlIfValid("")).toBeNull();
  expect(createUrlIfValid("not a url")).toBeNull();
  expect(createUrlIfValid("http://")).toBeNull();
});

export function isValidUrl(url: string) {
  return !!createUrlIfValid(url);
}
import.meta.vitest?.test("isValidUrl", ({ expect }) => {
  // Test with valid URLs
  expect(isValidUrl("https://example.com")).toBe(true);
  expect(isValidUrl("http://localhost:3000")).toBe(true);
  expect(isValidUrl("ftp://example.com")).toBe(true);

  // Test with invalid URLs
  expect(isValidUrl("")).toBe(false);
  expect(isValidUrl("not a url")).toBe(false);
  expect(isValidUrl("http://")).toBe(false);
});

export function isValidHostname(hostname: string) {
  // Basic validation
  if (!hostname || hostname.startsWith('.') || hostname.endsWith('.') || hostname.includes('..')) {
    return false;
  }

  const url = createUrlIfValid(`https://${hostname}`);
  if (!url) return false;
  return url.hostname === hostname;
}
import.meta.vitest?.test("isValidHostname", ({ expect }) => {
  // Test with valid hostnames
  expect(isValidHostname("example.com")).toBe(true);
  expect(isValidHostname("localhost")).toBe(true);
  expect(isValidHostname("sub.domain.example.com")).toBe(true);
  expect(isValidHostname("127.0.0.1")).toBe(true);

  // Test with invalid hostnames
  expect(isValidHostname("")).toBe(false);
  expect(isValidHostname("example.com/path")).toBe(false);
  expect(isValidHostname("https://example.com")).toBe(false);
  expect(isValidHostname("example com")).toBe(false);
});

export function isValidHostnameWithWildcards(hostname: string) {
  // Empty hostnames are invalid
  if (!hostname) return false;

  // Check if it contains wildcards
  const hasWildcard = hostname.includes('*');

  if (!hasWildcard) {
    // If no wildcards, validate as a normal hostname
    return isValidHostname(hostname);
  }

  // Basic validation checks that apply even with wildcards
  // - Hostname cannot start or end with a dot
  if (hostname.startsWith('.') || hostname.endsWith('.')) {
    return false;
  }

  // - No consecutive dots
  if (hostname.includes('..')) {
    return false;
  }

  // For wildcard validation, check that non-wildcard parts contain valid characters
  // Replace wildcards with a valid placeholder to check the rest
  const testHostname = hostname.replace(/\*+/g, 'wildcard');

  // Check if the resulting string would be a valid hostname
  if (!/^[a-zA-Z0-9.-]+$/.test(testHostname)) {
    return false;
  }

  // Additional check: ensure the pattern makes sense
  // Check each segment between wildcards
  const segments = hostname.split(/\*+/);
  for (let i = 0; i < segments.length; i++) {
    const segment = segments[i];
    if (segment === '') continue; // Empty segments are OK (consecutive wildcards)

    // First segment can't start with dot
    if (i === 0 && segment.startsWith('.')) {
      return false;
    }

    // Last segment can't end with dot
    if (i === segments.length - 1 && segment.endsWith('.')) {
      return false;
    }

    // No segment should have consecutive dots
    if (segment.includes('..')) {
      return false;
    }
  }

  return true;
}
import.meta.vitest?.test("isValidHostnameWithWildcards", ({ expect }) => {
  // Test with valid regular hostnames
  expect(isValidHostnameWithWildcards("example.com")).toBe(true);
  expect(isValidHostnameWithWildcards("localhost")).toBe(true);
  expect(isValidHostnameWithWildcards("sub.domain.example.com")).toBe(true);

  // Test with valid wildcard hostnames
  expect(isValidHostnameWithWildcards("*.example.com")).toBe(true);
  expect(isValidHostnameWithWildcards("a-*.example.com")).toBe(true);
  expect(isValidHostnameWithWildcards("*.*.org")).toBe(true);
  expect(isValidHostnameWithWildcards("**.example.com")).toBe(true);
  expect(isValidHostnameWithWildcards("sub.**.com")).toBe(true);
  expect(isValidHostnameWithWildcards("*-api.*.com")).toBe(true);

  // Test with invalid hostnames
  expect(isValidHostnameWithWildcards("")).toBe(false);
  expect(isValidHostnameWithWildcards("example.com/path")).toBe(false);
  expect(isValidHostnameWithWildcards("https://example.com")).toBe(false);
  expect(isValidHostnameWithWildcards("example com")).toBe(false);
  expect(isValidHostnameWithWildcards(".example.com")).toBe(false);
  expect(isValidHostnameWithWildcards("example.com.")).toBe(false);
  expect(isValidHostnameWithWildcards("example..com")).toBe(false);
  expect(isValidHostnameWithWildcards("*.example..com")).toBe(false);
});

export function matchHostnamePattern(pattern: string, hostname: string): boolean {
  // If no wildcards, it's an exact match
  if (!pattern.includes('*')) {
    return pattern === hostname;
  }

  // Convert the pattern to a regex
  // First, escape all regex special characters except *
  let regexPattern = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&');

  // Use a placeholder for ** to handle it separately from single *
  const doubleWildcardPlaceholder = '\x00DOUBLE_WILDCARD\x00';
  regexPattern = regexPattern.replace(/\*\*/g, doubleWildcardPlaceholder);

  // Replace single * with a pattern that matches anything except dots
  regexPattern = regexPattern.replace(/\*/g, '[^.]*');

  // Replace the double wildcard placeholder with a pattern that matches anything including dots
  regexPattern = regexPattern.replace(new RegExp(doubleWildcardPlaceholder, 'g'), '.*');

  // Anchor the pattern to match the entire hostname
  regexPattern = '^' + regexPattern + '$';

  try {
    const regex = new RegExp(regexPattern);
    return regex.test(hostname);
  } catch {
    return false;
  }
}
import.meta.vitest?.test("matchHostnamePattern", ({ expect }) => {
  // Test exact matches
  expect(matchHostnamePattern("example.com", "example.com")).toBe(true);
  expect(matchHostnamePattern("example.com", "other.com")).toBe(false);

  // Test single wildcard matches
  expect(matchHostnamePattern("*.example.com", "api.example.com")).toBe(true);
  expect(matchHostnamePattern("*.example.com", "www.example.com")).toBe(true);
  expect(matchHostnamePattern("*.example.com", "example.com")).toBe(false);
  expect(matchHostnamePattern("*.example.com", "api.v2.example.com")).toBe(false);

  // Test double wildcard matches
  expect(matchHostnamePattern("**.example.com", "api.example.com")).toBe(true);
  expect(matchHostnamePattern("**.example.com", "api.v2.example.com")).toBe(true);
  expect(matchHostnamePattern("**.example.com", "a.b.c.example.com")).toBe(true);
  expect(matchHostnamePattern("**.example.com", "example.com")).toBe(false);

  // Test complex patterns
  expect(matchHostnamePattern("api-*.example.com", "api-v1.example.com")).toBe(true);
  expect(matchHostnamePattern("api-*.example.com", "api-v2.example.com")).toBe(true);
  expect(matchHostnamePattern("api-*.example.com", "api.example.com")).toBe(false);
  expect(matchHostnamePattern("*.*.org", "mail.example.org")).toBe(true);
  expect(matchHostnamePattern("*.*.org", "example.org")).toBe(false);
});

export function isLocalhost(urlOrString: string | URL) {
  const url = createUrlIfValid(urlOrString);
  if (!url) return false;
  if (url.hostname === "localhost" || url.hostname.endsWith(".localhost")) return true;
  if (url.hostname.match(/^127\.\d+\.\d+\.\d+$/)) return true;
  return false;
}
import.meta.vitest?.test("isLocalhost", ({ expect }) => {
  // Test with localhost URLs
  expect(isLocalhost("http://localhost")).toBe(true);
  expect(isLocalhost("https://localhost:8080")).toBe(true);
  expect(isLocalhost("http://sub.localhost")).toBe(true);
  expect(isLocalhost("http://127.0.0.1")).toBe(true);
  expect(isLocalhost("http://127.1.2.3")).toBe(true);

  // Test with non-localhost URLs
  expect(isLocalhost("https://example.com")).toBe(false);
  expect(isLocalhost("http://192.168.1.1")).toBe(false);
  expect(isLocalhost("http://10.0.0.1")).toBe(false);

  // Test with URL objects
  expect(isLocalhost(new URL("http://localhost"))).toBe(true);
  expect(isLocalhost(new URL("https://example.com"))).toBe(false);

  // Test with invalid URLs
  expect(isLocalhost("not a url")).toBe(false);
  expect(isLocalhost("")).toBe(false);
});

export function isRelative(url: string) {
  const randomDomain = `${generateSecureRandomString()}.stack-auth.example.com`;
  const u = createUrlIfValid(url, `https://${randomDomain}`);
  if (!u) return false;
  if (u.host !== randomDomain) return false;
  if (u.protocol !== "https:") return false;
  return true;
}
import.meta.vitest?.test("isRelative", ({ expect }) => {
  // We can't easily mock generateSecureRandomString in this context
  // but we can still test the function's behavior

  // Test with relative URLs
  expect(isRelative("/")).toBe(true);
  expect(isRelative("/path")).toBe(true);
  expect(isRelative("/path?query=value#hash")).toBe(true);

  // Test with absolute URLs
  expect(isRelative("https://example.com")).toBe(false);
  expect(isRelative("http://example.com")).toBe(false);
  expect(isRelative("//example.com")).toBe(false);

  // Note: The implementation treats empty strings and invalid URLs as relative
  // This is because they can be resolved against a base URL
  expect(isRelative("")).toBe(true);
  expect(isRelative("not a url")).toBe(true);
});

export function getRelativePart(url: URL) {
  return url.pathname + url.search + url.hash;
}
import.meta.vitest?.test("getRelativePart", ({ expect }) => {
  // Test with various URLs
  expect(getRelativePart(new URL("https://example.com"))).toBe("/");
  expect(getRelativePart(new URL("https://example.com/path"))).toBe("/path");
  expect(getRelativePart(new URL("https://example.com/path?query=value"))).toBe("/path?query=value");
  expect(getRelativePart(new URL("https://example.com/path#hash"))).toBe("/path#hash");
  expect(getRelativePart(new URL("https://example.com/path?query=value#hash"))).toBe("/path?query=value#hash");

  // Test with different domains but same paths
  const url1 = new URL("https://example.com/path?query=value#hash");
  const url2 = new URL("https://different.com/path?query=value#hash");
  expect(getRelativePart(url1)).toBe(getRelativePart(url2));
});

/**
 * A template literal tag that returns a URL.
 *
 * Any values passed are encoded.
 */
export function url(strings: TemplateStringsArray | readonly string[], ...values: (string | number | boolean)[]): URL {
  return new URL(urlString(strings, ...values));
}
import.meta.vitest?.test("url", ({ expect }) => {
  // Test with no interpolation
  expect(url`https://example.com`).toBeInstanceOf(URL);
  expect(url`https://example.com`.href).toBe("https://example.com/");

  // Test with string interpolation
  expect(url`https://example.com/${"path"}`).toBeInstanceOf(URL);
  expect(url`https://example.com/${"path"}`.pathname).toBe("/path");

  // Test with number interpolation
  expect(url`https://example.com/${42}`).toBeInstanceOf(URL);
  expect(url`https://example.com/${42}`.pathname).toBe("/42");

  // Test with boolean interpolation
  expect(url`https://example.com/${true}`).toBeInstanceOf(URL);
  expect(url`https://example.com/${true}`.pathname).toBe("/true");

  // Test with special characters in interpolation
  expect(url`https://example.com/${"path with spaces"}`).toBeInstanceOf(URL);
  expect(url`https://example.com/${"path with spaces"}`.pathname).toBe("/path%20with%20spaces");

  // Test with multiple interpolations
  expect(url`https://example.com/${"path"}?query=${"value"}`).toBeInstanceOf(URL);
  expect(url`https://example.com/${"path"}?query=${"value"}`.pathname).toBe("/path");
  expect(url`https://example.com/${"path"}?query=${"value"}`.search).toBe("?query=value");
});


/**
 * A template literal tag that returns a URL string.
 *
 * Any values passed are encoded.
 */
export function urlString(strings: TemplateStringsArray | readonly string[], ...values: (string | number | boolean)[]): string {
  return templateIdentity(strings, ...values.map(encodeURIComponent));
}
import.meta.vitest?.test("urlString", ({ expect }) => {
  // Test with no interpolation
  expect(urlString`https://example.com`).toBe("https://example.com");

  // Test with string interpolation
  expect(urlString`https://example.com/${"path"}`).toBe("https://example.com/path");

  // Test with number interpolation
  expect(urlString`https://example.com/${42}`).toBe("https://example.com/42");

  // Test with boolean interpolation
  expect(urlString`https://example.com/${true}`).toBe("https://example.com/true");

  // Test with special characters in interpolation
  expect(urlString`https://example.com/${"path with spaces"}`).toBe("https://example.com/path%20with%20spaces");
  expect(urlString`https://example.com/${"?&="}`).toBe("https://example.com/%3F%26%3D");

  // Test with multiple interpolations
  expect(urlString`https://example.com/${"path"}?query=${"value"}`).toBe("https://example.com/path?query=value");
  expect(urlString`https://example.com/${"path"}?query=${"value with spaces"}`).toBe("https://example.com/path?query=value%20with%20spaces");
});

export function isChildUrl(parentUrl: URL, maybeChildUrl: URL) {
  return parentUrl.origin === maybeChildUrl.origin
    && isChildPath(parentUrl.pathname, maybeChildUrl.pathname)
    && [...parentUrl.searchParams].every(([key, value]) => maybeChildUrl.searchParams.get(key) === value)
    && (!parentUrl.hash || parentUrl.hash === maybeChildUrl.hash);
}
import.meta.vitest?.test("isChildUrl", ({ expect }) => {
  // true
  expect(isChildUrl(new URL("https://abc.com/"), new URL("https://abc.com/"))).toBe(true);
  expect(isChildUrl(new URL("https://abc.com/"), new URL("https://abc.com/path"))).toBe(true);
  expect(isChildUrl(new URL("https://abc.com/"), new URL("https://abc.com/path?query=value"))).toBe(true);
  expect(isChildUrl(new URL("https://abc.com/"), new URL("https://abc.com/path?query=value#hash"))).toBe(true);

  // false
  expect(isChildUrl(new URL("https://abc.com"), new URL("https://example.com"))).toBe(false);
  expect(isChildUrl(new URL("https://abc.com/"), new URL("https://example.com/path"))).toBe(false);
  expect(isChildUrl(new URL("https://abc.com/"), new URL("https://example.com/path?query=value"))).toBe(false);
  expect(isChildUrl(new URL("https://abc.com/"), new URL("https://example.com/path?query=value#hash"))).toBe(false);
  expect(isChildUrl(new URL("https://example.com"), new URL("https://abc.com/path?query=value#hash"))).toBe(false);
  expect(isChildUrl(new URL("https://example.com?query=value123"), new URL("https://example.com/path?query=value#hash"))).toBe(false);
});

export function isChildPath(parentPath: string, maybeChildPath: string) {
  parentPath = parentPath.endsWith("/") ? parentPath : parentPath + "/";
  maybeChildPath = maybeChildPath.endsWith("/") ? maybeChildPath : maybeChildPath + "/";
  return maybeChildPath.startsWith(parentPath);
}
import.meta.vitest?.test("isSubPath", ({ expect }) => {
  expect(isChildPath("/", "/")).toBe(true);
  expect(isChildPath("/", "/path")).toBe(true);
  expect(isChildPath("/path", "/")).toBe(false);
  expect(isChildPath("/path", "/path")).toBe(true);
  expect(isChildPath("/path/", "/path")).toBe(true);
  expect(isChildPath("/path", "/path/")).toBe(true);
  expect(isChildPath("/path/", "/path/")).toBe(true);
  expect(isChildPath("/path", "/path/abc")).toBe(true);
  expect(isChildPath("/path/", "/path/abc")).toBe(true);
  expect(isChildPath("/path", "/path-abc")).toBe(false);
  expect(isChildPath("/path", "/path-abc/")).toBe(false);
  expect(isChildPath("/path/", "/path-abc")).toBe(false);
  expect(isChildPath("/path/", "/path-abc/")).toBe(false);
});
